package com.jeesite.modules.sd.common.util;

import com.alibaba.fastjson.JSON;
import com.google.zxing.*;
import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.common.HybridBinarizer;
import com.google.zxing.qrcode.QRCodeWriter;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import lombok.extern.slf4j.Slf4j;

import javax.imageio.ImageIO;
import javax.swing.filechooser.FileSystemView;
import java.awt.*;
import java.awt.font.TextAttribute;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URL;
import java.util.HashMap;
import java.util.Hashtable;
import java.util.Map;

/**
 * 二维码、条形码工具类
 *
 * @author boya
 */
@Slf4j
public class QRBarCodeUtil {

    /**
     * CODE_WIDTH：二维码宽度，单位像素
     */
    private static final int CODE_WIDTH = 500;
    /**
     * CODE_HEIGHT：二维码高度，单位像素
     */
    private static final int CODE_HEIGHT = 500;
    /**
     * FRONT_COLOR：二维码前景色，0x000000 表示黑色
     */
    private static final int FRONT_COLOR = 0x000000;
    /**
     * BACKGROUND_COLOR：二维码背景色，0xFFFFFF 表示白色
     */
    private static final int BACKGROUND_COLOR = 0xFFFFFF;

    /**
     * 创建二维码图片到文件夹
     *
     * @param codeContent        二维码参数内容，如果是一个网页地址，如 https://www.baidu.com/ 则 微信扫一扫会直接进入此地址;
     *                           如果是一些参数，如 1541656080837，则微信扫一扫会直接回显这些参数值
     * @param codeImgFileSaveDir 二维码图片保存的目录,如 D:/codes
     * @param fileName           二维码图片文件名称，带格式,如 123.png
     * @throws IOException
     * @throws WriterException
     */
    public static void createCodeToFile(String codeContent, File codeImgFileSaveDir, String fileName) throws IOException, WriterException {
        // 参数检验
        if (codeContent == null || "".equals(codeContent.trim())) {
            // log.error("二维码内容为空，不进行操作...");
            return;
        }
        codeContent = codeContent.trim();

        if (codeImgFileSaveDir == null || codeImgFileSaveDir.isFile()) {
            codeImgFileSaveDir = FileSystemView.getFileSystemView().getHomeDirectory();
            //  log.warn("二维码图片存在目录为空，默认放在桌面...");
        }
        if (!codeImgFileSaveDir.exists()) {
            // log.info("二维码图片存在目录不存在，开始创建...");
            boolean mkdirs = codeImgFileSaveDir.mkdirs();
            if (mkdirs) {
                //    log.info("二维码图片存在目录创建成功...");
            } else {
                //   log.error("二维码图片存在目录创建失败...");
                return;
            }
        }
        if (fileName == null || "".equals(fileName)) {
            fileName = System.currentTimeMillis() + ".png";
            //  log.info("二维码图片文件名为空，随机生成 png 格式图片...");
        }

        /*com.google.zxing.EncodeHintType：编码提示类型,枚举类型
         * EncodeHintType.CHARACTER_SET：设置字符编码类型
         * EncodeHintType.ERROR_CORRECTION：设置误差校正
         *      ErrorCorrectionLevel：误差校正等级，L = ~7% correction、M = ~15% correction、Q = ~25% correction、H = ~30% correction
         *      不设置时，默认为 L 等级，等级不一样，生成的图案不同，但扫描的结果是一样的
         * EncodeHintType.MARGIN：设置二维码边距，单位像素，值越小，二维码距离四周越近
         * */
        Map<EncodeHintType, Object> hints = new HashMap();
        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
        hints.put(EncodeHintType.MARGIN, 1);

        /*
         * MultiFormatWriter:多格式写入，这是一个工厂类，里面重载了两个 encode 方法，用于写入条形码或二维码
         *      encode(String contents,BarcodeFormat format,int width, int height,Map<EncodeHintType,?> hints)
         *      contents:条形码/二维码内容
         *      format：编码类型，如 条形码，二维码 等
         *      width：码的宽度
         *      height：码的高度
         *      hints：码内容的编码类型
         * BarcodeFormat：枚举该程序包已知的条形码格式，即创建何种码，如 1 维的条形码，2 维的二维码 等
         * BitMatrix：位(比特)矩阵或叫2D矩阵，也就是需要的二维码
         */
        MultiFormatWriter multiFormatWriter = new MultiFormatWriter();
        BitMatrix bitMatrix = multiFormatWriter.encode(codeContent, BarcodeFormat.QR_CODE, CODE_WIDTH, CODE_HEIGHT, hints);

        /*java.awt.image.BufferedImage：具有图像数据的可访问缓冲图像，实现了 RenderedImage 接口
         * BitMatrix 的 get(int x, int y) 获取比特矩阵内容，指定位置有值，则返回true，将其设置为前景色，否则设置为背景色
         * BufferedImage 的 setRGB(int x, int y, int rgb) 方法设置图像像素
         *      x：像素位置的横坐标，即列
         *      y：像素位置的纵坐标，即行
         *      rgb：像素的值，采用 16 进制,如 0xFFFFFF 白色
         */
        BufferedImage bufferedImage = new BufferedImage(CODE_WIDTH, CODE_HEIGHT, BufferedImage.TYPE_INT_BGR);
        for (int x = 0; x < CODE_WIDTH; x++) {
            for (int y = 0; y < CODE_HEIGHT; y++) {
                bufferedImage.setRGB(x, y, bitMatrix.get(x, y) ? FRONT_COLOR : BACKGROUND_COLOR);
            }
        }

        /*
         * javax.imageio.ImageIO java 扩展的图像IO
         * write(RenderedImage im,String formatName,File output)
         *      im：待写入的图像
         *      formatName：图像写入的格式
         *      output：写入的图像文件，文件不存在时会自动创建
         *
         * 即将保存的二维码图片文件*/
        File codeImgFile = new File(codeImgFileSaveDir, fileName);
        ImageIO.write(bufferedImage, "png", codeImgFile);

        // log.info("二维码图片生成成功：{}", codeImgFile.getPath());
    }

    /**
     * 生成二维码 并 输出到输出流————通常用于输出到网页上进行显示
     * 输出到网页与输出到磁盘上的文件中，区别在于最后一句 ImageIO.write
     * write(RenderedImage im,String formatName,File output)：写到文件中
     * write(RenderedImage im,String formatName,OutputStream output)：输出到输出流中
     *
     * @param codeContent  ：二维码内容
     * @param outputStream ：输出流，比如 HttpServletResponse 的 getOutputStream
     * @throws IOException
     * @throws WriterException
     */
    public static void createCodeToOutputStream(String codeContent, OutputStream outputStream) throws IOException, WriterException {
        // 参数检验
        if (codeContent == null || "".equals(codeContent.trim())) {
            //  log.error("二维码内容为空，不进行操作...");
            return;
        }
        codeContent = codeContent.trim();

        /*
         * com.google.zxing.EncodeHintType：编码提示类型,枚举类型
         * EncodeHintType.CHARACTER_SET：设置字符编码类型
         * EncodeHintType.ERROR_CORRECTION：设置误差校正
         *      ErrorCorrectionLevel：误差校正等级，L = ~7% correction、M = ~15% correction、Q = ~25% correction、H = ~30% correction
         *      不设置时，默认为 L 等级，等级不一样，生成的图案不同，但扫描的结果是一样的
         * EncodeHintType.MARGIN：设置二维码边距，单位像素，值越小，二维码距离四周越近
         * */
        Map<EncodeHintType, Object> hints = new HashMap();
        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
        hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
        hints.put(EncodeHintType.MARGIN, 1);

        /*
         * MultiFormatWriter:多格式写入，这是一个工厂类，里面重载了两个 encode 方法，用于写入条形码或二维码
         *      encode(String contents,BarcodeFormat format,int width, int height,Map<EncodeHintType,?> hints)
         *      contents:条形码/二维码内容
         *      format：编码类型，如 条形码，二维码 等
         *      width：码的宽度
         *      height：码的高度
         *      hints：码内容的编码类型
         * BarcodeFormat：枚举该程序包已知的条形码格式，即创建何种码，如 1 维的条形码，2 维的二维码 等
         * BitMatrix：位(比特)矩阵或叫2D矩阵，也就是需要的二维码
         */
        MultiFormatWriter multiFormatWriter = new MultiFormatWriter();
        BitMatrix bitMatrix = multiFormatWriter.encode(codeContent, BarcodeFormat.QR_CODE, CODE_WIDTH, CODE_HEIGHT, hints);

        /*
         * java.awt.image.BufferedImage：具有图像数据的可访问缓冲图像，实现了 RenderedImage 接口
         * BitMatrix 的 get(int x, int y) 获取比特矩阵内容，指定位置有值，则返回true，将其设置为前景色，否则设置为背景色
         * BufferedImage 的 setRGB(int x, int y, int rgb) 方法设置图像像素
         *      x：像素位置的横坐标，即列
         *      y：像素位置的纵坐标，即行
         *      rgb：像素的值，采用 16 进制,如 0xFFFFFF 白色
         */
        BufferedImage bufferedImage = new BufferedImage(CODE_WIDTH, CODE_HEIGHT, BufferedImage.TYPE_INT_BGR);
        for (int x = 0; x < CODE_WIDTH; x++) {
            for (int y = 0; y < CODE_HEIGHT; y++) {
                bufferedImage.setRGB(x, y, bitMatrix.get(x, y) ? FRONT_COLOR : BACKGROUND_COLOR);
            }
        }

        // 区别就是以一句，输出到输出流中，如果第三个参数是 File，则输出到文件中
        ImageIO.write(bufferedImage, "png", outputStream);

        //log.info("二维码图片生成到输出流成功...");
    }

    /**
     * 生成带文字的二维码
     *
     * @param codeContent  二维码内容
     * @param width        二维码宽度
     * @param height       二维码高度
     * @param filePath     文件路径及名字
     * @param head         顶部文字
     * @param foot         第一行底部文字
     * @param foot1        第二行底部文字
     * @param fontSize     文字号
     * @param outputStream 输出流
     */
    private static void createQRCodeWithWords(String codeContent, int width, int height, String filePath
            , String head, String foot, String foot1, int fontSize, OutputStream outputStream) throws WriterException, IOException {
        QRCodeWriter qrCodeWriter = new QRCodeWriter();

        Map<EncodeHintType, Object> hints = new HashMap<>();
        //设置二维码周围空白间隙
        hints.put(EncodeHintType.MARGIN, 10);
        hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
        BitMatrix bitMatrix = qrCodeWriter.encode(codeContent, BarcodeFormat.QR_CODE, width, height, hints);
        //转换为BufferedImage用于新建画布加文字
        BufferedImage image = MatrixToImageWriter.toBufferedImage(bitMatrix);
        //新建图片
        BufferedImage outImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB);
        //创建画板
        Graphics2D ghs = outImage.createGraphics();

        //设置ghs一些属性
        setGraphics2D(ghs);
        // 画二维码到新图片
        ghs.drawImage(image, 0, 50, image.getWidth(), image.getHeight(), null);
        // 设置字体颜色
        ghs.setColor(Color.BLACK);
        // 设置字体、字型、字号、间距 Should be one of: Font.PLAIN, Font.BOLD, Font.ITALIC or their combination
        Font font = new Font("微软雅黑", Font.PLAIN, fontSize);
        Map<TextAttribute, Object> attributes = new HashMap<>();
        attributes.put(TextAttribute.TRACKING, 0.15);
        Font font2 = font.deriveFont(attributes);
        ghs.setFont(font2);
        // 标题文字长度
        int strWidth = ghs.getFontMetrics().stringWidth(head);

        // 底部第二行文字长度
        int strWidthFoot2 = ghs.getFontMetrics().stringWidth(foot1);
        int strWidthFoot;

        //计算长度使其居中
        strWidthFoot = strWidth - strWidthFoot2 >= 0 ? strWidth : strWidthFoot2;
        int wordStartXFoot = (width - strWidthFoot) / 2;

        //第一排
        ghs.drawString(head, wordStartXFoot, fontSize+10);
        //第二排
        ghs.drawString(foot, wordStartXFoot, 2*fontSize+15);
        //第三排
        ghs.drawString(foot1, wordStartXFoot, 3*fontSize+20);
        //释放画板资源
        ghs.dispose();
        outImage.flush();

        // 判断要用哪种输出
        if (null == outputStream) {
            File outPutFile = new File(filePath);
            ImageIO.write(outImage, "png", outPutFile);
        } else {
            ImageIO.write(outImage, "png", outputStream);
        }
    }

    /**
     * 生成带文字的二维码到本地
     *
     * @param codeContent 二维码内容
     * @param width       二维码宽度
     * @param height      二维码高度
     * @param filePath    文件路径及名字
     * @param head        顶部文字
     * @param foot        第一行底部文字
     * @param foot1       第二行底部文字
     * @param fontSize    文字号
     * @throws WriterException
     * @throws IOException
     */
    public static void createQRCodeWithWordsToFile(String codeContent, int width, int height, String filePath
            , String head, String foot, String foot1, int fontSize) throws WriterException, IOException {
        createQRCodeWithWords(codeContent, width, height, filePath, head, foot, foot1, fontSize, null);
    }

    /**
     * 生成带文字的二维码到输出流
     *
     * @param codeContent  二维码内容
     * @param width        二维码宽度
     * @param height       二维码高度
     * @param fullNum      顶部文字(文号)
     * @param foot         第一行底部文字
     * @param foot1        第二行底部文字
     * @param fontSize     文字号
     * @param outputStream 输出流
     * @throws WriterException
     * @throws IOException
     */
    public static void createQRCodeWithWordsToStream(String codeContent, int width, int height, String fullNum
            , String foot, String foot1, int fontSize, OutputStream outputStream) throws WriterException, IOException {
        createQRCodeWithWords(codeContent, width, height, null, fullNum, foot, foot1, fontSize, outputStream);
    }

    /**
     * 设置 Graphics2D 属性  （抗锯齿）
     *
     * @param graphics2D
     */
    private static void setGraphics2D(Graphics2D graphics2D) {
        graphics2D.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        graphics2D.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        Stroke s = new BasicStroke(1, BasicStroke.CAP_ROUND, BasicStroke.JOIN_MITER);
        graphics2D.setStroke(s);
    }

    /**
     * 解析二维码(参数三选一，空参传NULL)
     *
     * @param url           网络二维码图片链接。
     * @param file          本地文件
     * @param bufferedImage 缓冲图像
     * @return
     * @throws NotFoundException
     * @throws IOException
     */
    public static String parseQRCode(URL url, File file, BufferedImage bufferedImage) throws
            NotFoundException, IOException {

        String resultStr;
        if (url == null && (file == null || file.isDirectory() || !file.exists()) && bufferedImage == null) {
            return null;
        }

        /*
         * ImageIO 的 BufferedImage read(URL input) 方法用于读取网络图片文件转为内存缓冲图像
         * 同理还有：read(File input)、read(InputStream input)、、read(ImageInputStream stream)
         *
         * 如果图片网络地址错误，比如不能访问，则 read 抛异常：javax.imageio.IIOException: Can't get input stream from URL!
         */
        if (url != null) {

            bufferedImage = ImageIO.read(url);
        } else if (bufferedImage == null) {

            bufferedImage = ImageIO.read(file);
        }

        /*
         * com.google.zxing.client.j2se.BufferedImageLuminanceSource：缓冲图像亮度源
         * 将 java.awt.image.BufferedImage 转为 zxing 的 缓冲图像亮度源
         * 关键就是下面这几句：HybridBinarizer 用于读取二维码图像数据，BinaryBitmap 二进制位图
         */
        BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
        BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
        Hashtable hints = new Hashtable();

        /*
         * 如果内容包含中文，则解码的字符集格式应该和编码时一致
         */
        hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");

        /*
         * 如果图片不是二维码图片，则 decode 抛异常：com.google.zxing.NotFoundException
         * MultiFormatWriter 的 encode 用于对内容进行编码成 2D 矩阵
         * MultiFormatReader 的 decode 用于读取二进制位图数据
         */
        Result result = new MultiFormatReader().decode(bitmap, hints);
        resultStr = result.getText();
        return resultStr;
    }

    /**
     * 解析二维码转为JSON对象(前三个参数三选一，空参传NULL；clazz非空)
     *
     * @param url           网络二维码图片链接。
     * @param file          本地文件
     * @param bufferedImage 缓冲图像
     * @param clazz         类
     * @param <T>           对象
     * @return
     * @throws NotFoundException
     * @throws IOException
     */
    public static <T> T parseJSONQRCode(URL url, File file, BufferedImage bufferedImage, Class<T> clazz) throws
            NotFoundException, IOException {
        if (clazz == null) {
            return null;
        }
        String qrCode = parseQRCode(url, file, bufferedImage);
        // log.info("未经过JSON解析：{}", qrCode);
        return JSON.parseObject(qrCode, clazz);
    }


}
